
// Copyright (c) 2006 Michael Eddington
// Michael Eddington <meddington@gmail.com>

// Based on code from MSDN arcitle available at:
//   http://msdn.microsoft.com/msdnmag/issues/03/07/XMLSchemaValidation/
//
// Origional implementation by:
//   Dan Sullivan <http://staff.develop.com/dsullivan/> 
//   Aaron Skonnard <http://staff.develop.com/aarons>

// $Id: SoapValidation.cs 36 2006-11-05 13:04:40Z dd $

using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using System.Text;
using System.IO;
using System.Web;
using System.Web.Services.Protocols;
using System.Web.Services.Description;
using System.Web.Services.Configuration;
using System.Xml.Serialization;
using System.Resources;

namespace SoapValidation
{
	/// <summary>
	/// XML namespaces used in assembly
	/// </summary>
	public class Namespaces
	{
		public const string DM = "http://www.develop.com/web/services/";
		public const string XMLNS = "http://www.w3.org/2000/xmlns/";
	}

	/// <summary>
	/// intializer object generated by GetInitializer, stored by the 
	/// infrastructure, and provided to each call to Initialize
	/// holds state used to perform schema/assertion validation
	/// </summary>
	internal class ValidationContext
	{
		internal XmlNamespaceManager NamespaceManager = null;
		internal AssertAttribute[] AssertAttributes = null;
		internal XPathExpression CompleteRuleExpression = null;
		internal ValidationAttribute ValidationAttribute = null;
		internal XmlSchemaSet SchemaCollection = null;
	}


	/// <summary>
	/// ValidationExtension implements both schema validation and assertion processing
	/// </summary>
	public class ValidationExtension : SoapExtension
	{
		/// <summary>
		/// initializer object (context for each request)
		/// generated by GetInitializer
		/// </summary>
		ValidationContext _context;

		// helpers for loading schema files from different locations
		
		internal void LoadSchemaFromFile(XmlSchemaSet sc, string filePath)
		{
			XmlTextReader schemaReader = new XmlTextReader(filePath);
			XmlSchema schema = XmlSchema.Read(schemaReader, null);
			sc.Add(schema);
		}

		internal void LoadSchemasFromServiceDescriptions(XmlSchemaSet sc, ServiceDescription sd)
		{
			foreach (XmlSchema embeddedXsd in sd.Types.Schemas)
				sc.Add(embeddedXsd);
		}

		internal void LoadSchemasFromDirectory(XmlSchemaSet sc, string relativeDir)
		{
			if (Directory.Exists(HttpContext.Current.Server.MapPath(relativeDir)))
			{
				string[] schemaFiles = Directory.GetFiles(HttpContext.Current.Server.MapPath(relativeDir), "*.xsd");
				foreach (string schemaFile in schemaFiles)
					LoadSchemaFromFile(sc, schemaFile);

				string[] wsdlFiles = Directory.GetFiles(HttpContext.Current.Server.MapPath(relativeDir), "*.wsdl");
				foreach (string wsdlFile in wsdlFiles)
				{
					LoadSchemasFromServiceDescriptions(sc, ServiceDescription.Read(wsdlFile));
				}
			}
		}

		internal void LoadReflectedSchemas(XmlSchemaSet sc, Type type, string url)
		{
			ServiceDescriptionReflector r = new ServiceDescriptionReflector();
			r.Reflect(type, url);

			foreach (XmlSchema xsd in r.Schemas)
				sc.Add(xsd);

			foreach (ServiceDescription sd in r.ServiceDescriptions)
				LoadSchemasFromServiceDescriptions(sc, sd);
		}

		internal void LoadSchemaFromResourceFile(XmlSchemaSet sc, string root, 
			string assemblyName, string resourceName)
		{
			// load SOAP 1.1 schema from assembly resource file		
			ResourceManager resmgr = new ResourceManager(root, System.Reflection.Assembly.Load(assemblyName));
			string resSchema = resmgr.GetString(resourceName);
			StringReader sreader = new StringReader(resSchema);
			XmlTextReader xtr = new XmlTextReader(sreader);
			XmlSchema soapSchema = XmlSchema.Read(xtr, null);
			sc.Add(soapSchema);
			xtr.Close();
		}

		// called by both versions of GetInitializer to generated initializer object (ValidationContext)
		// which serves as the context for future requests
		public object GetInitializerHelper(Type serviceType, LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
		{
			ValidationContext ctx = new ValidationContext();
			HttpContext httpctx = HttpContext.Current;

			// cache ValidationAttribute for future use
			ctx.ValidationAttribute = (ValidationAttribute)attribute;

			// temporary document/navigator for compiling XPath expressions
			XmlDocument doc = new XmlDocument();
			XPathNavigator nav = doc.CreateNavigator();
			// namespace manager for holding all namespace bindings
			XmlNamespaceManager ns = new XmlNamespaceManager(nav.NameTable);

			// retrieve (user-provided) namespace binding attributes
			object[] namespaceAtts = serviceType.GetCustomAttributes(typeof(AssertNamespaceBindingAttribute), true);
			foreach (AssertNamespaceBindingAttribute nsa in namespaceAtts)
				ns.AddNamespace(nsa.Prefix, nsa.Namespace);
			// store namespace manager in context for future use
			ctx.NamespaceManager = ns;

			// retrieve (user-provided) assertion attributes
			AssertAttribute[] allRuleAtts = null;
			object[] classRuleAtts = serviceType.GetCustomAttributes(typeof(AssertAttribute), true);
			if (methodInfo != null) // enabled via custom SoapExtensionAttribute
			{
				// retrieve (user-provided) method and class-level assert attributes
				object[] methodRuleAtts = methodInfo.GetCustomAttributes(typeof(AssertAttribute));
				allRuleAtts = new AssertAttribute[methodRuleAtts.Length + classRuleAtts.Length];
				methodRuleAtts.CopyTo(allRuleAtts, 0);
				classRuleAtts.CopyTo(allRuleAtts, methodRuleAtts.Length);
			}
			else
				// just retrieve (user-provided) class-level assertion attributes
				allRuleAtts = (AssertAttribute[])classRuleAtts;

			// store all assertions in context
			ctx.AssertAttributes = allRuleAtts;

			// generate, compile, and cache XPath expressions for future use
			StringBuilder completeExpression = new StringBuilder();
			string and = "";
			foreach (AssertAttribute ra in allRuleAtts)
			{
				string rule = String.Format("boolean({0})", ra.Rule);
				// cache compiled expression for future use
				ra.Expression = nav.Compile(rule);
				ra.Expression.SetContext(ns);
				completeExpression.Append(and);
				completeExpression.Append(rule);
				and = " and ";
			}
			if (completeExpression.Length != 0)
			{
				// complete expression (combination off all asserts) for quick success/failure check
				ctx.CompleteRuleExpression = nav.Compile(completeExpression.ToString());
				ctx.CompleteRuleExpression.SetContext(ns);
			}
			
			// XML Schema cache for schema validation
			XmlSchemaSet sc = new XmlSchemaSet();

			// load SOAP 1.2 and 1.1 schema from assembly resource file
			LoadSchemaFromResourceFile(sc, "SoapValidation.ValidationResources", "SoapValidation", "soap_envelope");
			LoadSchemaFromResourceFile(sc, "SoapValidation.ValidationResources", "SoapValidation", "soap_envelope1_1");

			// load automatically-generated XML Schema for current endpoint (in case it hasn't already been loaded)
			LoadReflectedSchemas(sc, serviceType, httpctx.Request.RawUrl);

			// load from xsd directory by default
			LoadSchemasFromDirectory(sc, "xsd");
			// load schemas from user-defined vroot caches
			object[] schemaCacheAtts = serviceType.GetCustomAttributes(typeof(ValidationSchemaFolderAttribute), true);
			foreach (ValidationSchemaFolderAttribute schemaCacheLoc in schemaCacheAtts)
				LoadSchemasFromDirectory(sc, schemaCacheLoc.RelativeFolder);

			// load schemas from explicit (user-provided) locations
			object[] schemaLocations = serviceType.GetCustomAttributes(typeof(ValidationSchemaAttribute), true);
			foreach (ValidationSchemaAttribute vsa in schemaLocations)
				LoadSchemaFromFile(sc, httpctx.Server.MapPath(vsa.SchemaFile));

			// save schema cache for future use
			ctx.SchemaCollection = sc;

			// the System.Web.Services infrastructure will cache this object
			// and supply it in each future call to Initialize
			return ctx;
		}

		// this GetInitializer is called when the SoapExtension is registered
		// in the config file (web/machine.config), it's called the first time
		// the class is invoked
		public override object GetInitializer(System.Type serviceType)
		{
			return GetInitializerHelper(serviceType, null, null);
		}

		// this GetInitializer is called when the SoapExtension is configured
		// via a custom SoapExtensionAttribute, it's called the first time the
		// WebMethod is invoked
		public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
		{
			return GetInitializerHelper(methodInfo.DeclaringType, methodInfo, attribute);
		}

		// called each a givne [WebMethod] is used (will recieve a ValidationContext object)
		public override void Initialize(object initializer)
		{
			// business rules passed in by initialize
			_context = (ValidationContext)initializer;
		}

		// this will be called four times (before/after serialize/deserialize) for each [WebMethod] 
		public override void ProcessMessage(System.Web.Services.Protocols.SoapMessage message)
		{
			// this extension only cares about the BeforeSerialize stage where it
			// performs schema/assertion validation
			if (System.Web.Services.Protocols.SoapMessageStage.BeforeDeserialize ==
				message.Stage && _context != null)
			{
				try
				{
					// create an XPathNavigator on the request stream
					XmlReader reader = null;
					// check to see if the user wants schema validation
					if (_context.ValidationAttribute != null && _context.ValidationAttribute.SchemaValidation == true)
					{
						// configure XmlValidatingReader
						XmlTextReader tr = new XmlTextReader(message.Stream);
						XmlReaderSettings xmlReaderSettings = new XmlReaderSettings();
						xmlReaderSettings.Schemas = _context.SchemaCollection;
						XmlReader vr = XmlReader.Create(tr, xmlReaderSettings);

						// if the user doesn't want to check assertions, just 
						// rip through the stream to do schema validation
						// and return
						if (_context.ValidationAttribute.CheckAssertions == false)
						{
							while (vr.Read()) ; // read through stream
							return;
						}

						reader = vr;
					}
					else
						reader = new XmlTextReader(message.Stream);

					// turn the message stream into an XPathDocument
					// (will validate if it's an XmlValidatingReader)
					XPathDocument doc = new XPathDocument(reader);
					XPathNavigator nav = doc.CreateNavigator();

					// evaluate the XPath assertions - if complete expression returns true
					// let the System.Web.Services infrastructure finish processing 
					if ((_context.ValidationAttribute != null) &&
						(_context.ValidationAttribute.CheckAssertions == true) &&
						(_context.CompleteRuleExpression != null) &&
						(false == (bool)nav.Evaluate(_context.CompleteRuleExpression)))
					{
						// otherwise generate a SOAP fault indicating exactly which
						// assertions failed
						XmlDocument errorDoc = new XmlDocument();
						XmlElement detailElement = errorDoc.CreateElement(SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace);
						errorDoc.AppendChild(detailElement);
						XmlElement failedRulesElement = errorDoc.CreateElement("dm", "failedAssertions", Namespaces.DM);
						XmlAttribute nsDeclAttribute = null;
						// generate namespace declarations required by assert expressions
						foreach (string prefix in _context.NamespaceManager)
						{
							if (prefix.Equals("xml") || prefix.Equals("xmlns") || prefix.Equals(""))
								continue;
							nsDeclAttribute = errorDoc.CreateAttribute("xmlns", prefix, Namespaces.XMLNS);
							nsDeclAttribute.Value = _context.NamespaceManager.LookupNamespace(prefix);
							failedRulesElement.Attributes.Append(nsDeclAttribute);
						}
						detailElement.AppendChild(failedRulesElement);

						// check to see which assertions failed and list in failedRules
						foreach (AssertAttribute aa in _context.AssertAttributes)
						{
							if (false == (bool)nav.Evaluate(aa.Expression))
							{
								XmlElement assertElement = errorDoc.CreateElement("dm", "assert", Namespaces.DM);
								failedRulesElement.AppendChild(assertElement);
								XmlElement expElement = errorDoc.CreateElement("dm", "expression", Namespaces.DM);
								assertElement.AppendChild(expElement);
								expElement.InnerText = aa.Rule;
								XmlElement descElement = errorDoc.CreateElement("dm", "description", Namespaces.DM);
								assertElement.AppendChild(descElement);
								descElement.InnerText = aa.Description;
							}
						}

						// throw a SoapException with the detail element node we just built
						SoapException error = new SoapException(
							"Business rules failed validation",
							SoapException.ClientFaultCode,
							HttpContext.Current.Request.Url.AbsoluteUri,
							detailElement);
						throw error;
					}
				}
				finally
				{
					// make sure you leave the stream the way you found it
					message.Stream.Position = 0L;
				}
			}
		}
	}

	/// <summary>
	/// ValidationAttribute triggers schema/assert validation through
	/// the ValidationExtension class
	/// </summary>
	[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
	public class ValidationAttribute : SoapExtensionAttribute
	{
		int priority = 0;
		bool _schemaValidation = true;
		bool _checkAssertions = true;

		// enable schema validation?
		public bool SchemaValidation
		{
			get { return _schemaValidation; }
			set { _schemaValidation = value; }
		}

		// enable assertion processing?
		public bool CheckAssertions
		{
			get { return _checkAssertions; }
			set { _checkAssertions = value; }
		}

		// used by soap extension to get the type
		// of object to be created
		public override Type ExtensionType
		{
			get
			{
				// tell the System.Web.Services infrastructure
				// to use the ValidationExtension class
				return typeof(ValidationExtension);
			}
		}
		public override int Priority
		{
			get { return priority; }
			set { priority = value; }
		}
	}

	/// <summary>
	/// Specify additional folder containing schmea files.  Folder
	/// is relative to vroot.  Apply at to classes.
	/// </summary>
	[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
	public class ValidationSchemaFolderAttribute : Attribute
	{
		string _relativeFolder;

		/// <summary>
		/// Specify additional folder containing schmea files.  Folder
		/// is relative to vroot.  Apply at to classes.
		/// </summary>
		/// <param name="relativeFolder">Folder relative to vroot with additional schema files</param>
		public ValidationSchemaFolderAttribute(string relativeFolder)
		{
			_relativeFolder = relativeFolder;
		}

		public string RelativeFolder
		{
			get { return _relativeFolder; }
			set { _relativeFolder = value; }
		}
	}

	/// <summary>
	/// Specifies additional schema file to load (relative to vroot).
	/// Apply at class level.
	/// </summary>
	[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
	public class ValidationSchemaAttribute : Attribute
	{
		string _schemaFile;

		/// <summary>
		/// Specifies additional schema file to load (relative to vroot).
		/// </summary>
		/// <param name="schemaFile">Schema file to load (relative to vroot).</param>
		public ValidationSchemaAttribute(string schemaFile)
		{
			_schemaFile = schemaFile;
		}

		public string SchemaFile
		{
			get { return _schemaFile; }
		}
	}

	/// <summary>
	/// Specifies an XPath expression evaluated as an assertion (must evaluate to true)
	/// can be used on an individual method or the entire class - if used on the class, 
	/// the expression applies to all WebMethods.
	/// </summary>
	/// <remarks>
	/// An example of using the Assert
	/// <code>
	/// [AssertNamespaceBinding("s", 
	///   "http://schemas.xmlsoap.org/soap/envelope/")]
	/// [AssertNamespaceBinding("t", 
	///   "http://example.org/geometry/")]
	/// [WebService(Namespace="http://example.org/geometry/")]
	/// [Assert(@"//t:width >= 0", "width must be greater than 0")]
	/// [Assert("(//t:length > //t:width)", 
	///         "Length must be greater than width")]
	/// public class Geometry
	/// {
	///     [WebMethod]
	///     [Validation]
	///     [Assert("(//t:length * //t:width) > 100", 
	///             "Area must be greater than 100")]
	///     [Assert("(//t:length div //t:width) = 2", 
	///             "Length must be twice the size of width")]
	///     public double CalcArea(double length, double width)
	///     {
	///         return length * width;
	///     }
	///     [WebMethod]
	///     [Validation]
	///     public double CalcPerimeter(double length, double width)
	///     {
	///         return length * width;
	///     }
	/// }
	/// </code>
	/// </remarks>
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
	public class AssertAttribute : Attribute
	{
		string _rule;
		string _description;
		XPathExpression _expression;

		/// <summary>
		/// Empty constructor to allow for serialization.
		/// </summary>
		public AssertAttribute() : this("", "") { }

		/// <summary>
		/// Specifies an XPath expression evaluated as an assertion (must evaluate to true)
		/// can be used on an individual method or the entire class - if used on the class, 
		/// the expression applies to all WebMethods.
		/// </summary>
		/// <remarks>
		/// See class remarks for example of use.
		/// </remarks>
		/// <param name="rule">An XPath assertion rule (must evaluate to true)</param>
		/// <param name="description">Description of rule</param>
		public AssertAttribute(string rule, string description)
		{
			_rule = rule;
			_description = description;
			_expression = null;
		}

		/// <summary>
		/// Specifies an XPath expression evaluated as an assertion (must evaluate to true)
		/// can be used on an individual method or the entire class - if used on the class, 
		/// the expression applies to all WebMethods.
		/// </summary>
		/// <remarks>
		/// See class remarks for example of use.
		/// </remarks>
		/// <param name="rule">An XPath assertion rule (must evaluate to true)</param>
		/// <param name="description">Description of rule</param>
		public AssertAttribute(string rule) : this(rule, "") { }

		[XmlElement("expression")]
		public string Rule
		{
			get { return _rule; }
			set { _rule = value; }
		}

		[XmlElement("description")]
		public string Description
		{
			get { return _description; }
			set { _description = value; }
		}

		internal XPathExpression Expression
		{
			get { return _expression; }
			set { _expression = value; }
		}
	}

	/// <summary>
	/// Specifies namespace bindings required by the XPath (assert)
	/// expressions
	/// </summary>
	/// <remarks>
	/// An example of using the Assert
	/// <code>
	/// [AssertNamespaceBinding("s", 
	///   "http://schemas.xmlsoap.org/soap/envelope/")]
	/// [AssertNamespaceBinding("t", 
	///   "http://example.org/geometry/")]
	/// [WebService(Namespace="http://example.org/geometry/")]
	/// [Assert(@"//t:width >= 0", "width must be greater than 0")]
	/// [Assert("(//t:length > //t:width)", 
	///         "Length must be greater than width")]
	/// public class Geometry
	/// {
	///     [WebMethod]
	///     [Validation]
	///     [Assert("(//t:length * //t:width) > 100", 
	///             "Area must be greater than 100")]
	///     [Assert("(//t:length div //t:width) = 2", 
	///             "Length must be twice the size of width")]
	///     public double CalcArea(double length, double width)
	///     {
	///         return length * width;
	///     }
	///     [WebMethod]
	///     [Validation]
	///     public double CalcPerimeter(double length, double width)
	///     {
	///         return length * width;
	///     }
	/// }
	/// </code>
	/// </remarks>
	[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
	public class AssertNamespaceBindingAttribute : Attribute
	{
		string _prefix;
		string _ns;

		public AssertNamespaceBindingAttribute(string prefix, string ns)
		{
			_prefix = prefix;
			_ns = ns;
		}
		public string Prefix
		{
			get { return _prefix; }
		}
		public string Namespace
		{
			get { return _ns; }
		}
	}

	/// <summary>
	/// Format extension used to customize automatic WSDL generation (.asmx?wsdl)
	/// </summary>
	[XmlFormatExtension("validation", Namespaces.DM, typeof(InputBinding))]
	[XmlFormatExtensionPrefix("dm", Namespaces.DM)]
	public class ValidationFormatExtension : ServiceDescriptionFormatExtension
	{
		/// <summary>
		/// holds array of AssertAttributes (expression + description)
		/// </summary>
		[XmlArray("assertions")]
		[XmlArrayItem("assert")]
		public AssertAttribute[] assertions;

		/// <summary>
		/// holds namespace declarations required by XPath expressions
		/// </summary>
		[XmlNamespaceDeclarations]
		public XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
	}

	/// <summary>
	/// called by the runtime during WSDL generation (.asmx?wsdl)
	/// must be configured in the web/machine.config file
	/// </summary>
	public class ValidationExtensionReflector : SoapExtensionReflector
	{
		public override void ReflectMethod()
		{
			ProtocolReflector reflector = ReflectionContext;
			// ValidationFormatExtension represents an extension element
			ValidationFormatExtension fe = new ValidationFormatExtension();

			// populate with assertions and namespaces
			object va = reflector.Method.GetCustomAttribute(typeof(ValidationAttribute));
			if (va != null)
			{
				// retrieve assertions
				object[] classRuleAtts = reflector.Method.DeclaringType.GetCustomAttributes(typeof(AssertAttribute), true);
				object[] methodRuleAtts = reflector.Method.GetCustomAttributes(typeof(AssertAttribute));
				fe.assertions = new AssertAttribute[methodRuleAtts.Length + classRuleAtts.Length];
				methodRuleAtts.CopyTo(fe.assertions, 0);
				classRuleAtts.CopyTo(fe.assertions, methodRuleAtts.Length);

				// retrieve namespace bindings
				object[] namespaceAtts = reflector.Method.DeclaringType.GetCustomAttributes(typeof(AssertNamespaceBindingAttribute), true);
				foreach (AssertNamespaceBindingAttribute nsa in namespaceAtts)
					fe.ns.Add(nsa.Prefix, nsa.Namespace);
			}
			// inject extension element in the binding/input element
			reflector.OperationBinding.Input.Extensions.Add(fe);
		}
	}
}
